package ia1011.zanzibar.featurestructures;

import ia1011.zanzibar.rules.RegExElem;
import java.util.*;
import java.lang.reflect.*;

import org.apache.uima.jcas.JCas;
import org.apache.uima.jcas.cas.FSArray;
import org.apache.uima.jcas.tcas.Annotation;

/**
 * The class represents a feature structure
 * 
 * @author Alessandro Perrino (creator)
 * @author <br> Bastianelli E. emanuele.bastianelli@gmail.com (modifier)
 * @author <br> Caponi E. emiliano.caponi@gmail.com (modifier)
 * @author <br> Castellucci G. castellucci.giuseppe@gmail.com (modifier)
 * @author <br> Rossi A. berardino.viola@gmail.com (modifier)
 *  
 */
public class FeatureStructure extends RegExElem {
	Hashtable fields;	

	/**
	 * Create a new empty feature structure	 
	 */
	public FeatureStructure() {
		this.fields = new Hashtable();		
	}
	
	/**
	 * Insert a new feature in the feature structure
	 * @param featureName
	 * @param featureValue
	 * @return
	 */
	public Object put(String featureName, Object featureValue) {
		// method put of class Hashtable
		return (this.fields.put(featureName, featureValue)); 
	}
	
	/**
	 * 
	 * @param featureName
	 * @return the value of feature named featureName 
	 */
	public Object get(String featureName) {
		return this.getFields().get(featureName);
	}

	/**
	 * set the features in the feature structure
	 * @param features 
	 */
	public void setFields(Hashtable features) {
		this.fields = features;
	}

	/**
	 * 
	 * @return all the features of feature structure
	 */
	public Hashtable getFields() {
		return this.fields;
	}

	/**
	 * This method returns true if the first object subsumes the second. It is
	 * true when the second structure contains all the informations of the
	 * first.
	 * @param featureSturcture
	 * @return
	 */
	public boolean subsumption(Object featureSturcture) {
		Hashtable h = this.getFields();
		Enumeration e = h.keys();
		String name = (String) ((h.get("__CLASS_NAME__") == null ? "Any" : h.get("__CLASS_NAME__")));
		if ((name.equals(featureSturcture.getClass().getName()) || name.equals("Any"))) {
			String key;
			String methodName;
			Method m;
			while (e.hasMoreElements()) {
				key = (String) (e.nextElement());
				key = key.substring(0, 1).toUpperCase()
						+ key.substring(1, key.length());
				if (!(key.equals("Ref")) && (!(key.equals("__CLASS_NAME__")))) {
					methodName = "get" + key;
					try {
						m = featureSturcture.getClass().getMethod(methodName, null);
					} catch (NoSuchMethodException ex) {
						System.out.println("exception: " + ex.getMessage());
						return false;
					}
					Object v1 = h.get(key);
					Object v2 = null;
					try {
						v2 = m.invoke(featureSturcture, null);
					} catch (Exception ex) {
						System.out.println("exception: " + ex.getMessage());
						return false;
					}
					boolean b = false;
					// If the value to be subsumed is a Vector, every element
					// has to be checked
					int n = 1;
					for (int i = 0; i < n; i++) {
						if ((v1 instanceof String) && (v2 instanceof String)) {
							b = v1.equals(v2);
						} else if (v1 instanceof FeatureStructure) {
							b = ((FeatureStructure) v1).subsumption(v2);
						} else if (v1 instanceof Variable) {
							Object vval = ((Variable) v1).getValue();
							if (((Variable) v1).isEmpty()) {
								((Variable) v1).setValue(v2);
								b = true;
							} else if (!(vval instanceof java.lang.String)) {
								Method mSub;
								try {
									Class[] args = { Object.class };
									mSub = vval.getClass().getMethod(
											"subsumption", args);
								} catch (NoSuchMethodException ex) {
									System.out.println("exception: "
											+ ex.getMessage());
									return false;
								}
								try {
									Object[] args = { v2 };
									b = ((Boolean) (mSub.invoke(vval, args)))
											.booleanValue();
								} catch (Exception ex) {
									System.out.println("exception: "
											+ ex.getMessage());
									return false;
								}
							} else if (v2 instanceof String) {
								b = ((String) vval).equals((String) v2);
							}
						} else if (v1.getClass().getName()
								.equals(v2.getClass().getName())) {
							Method mSub;
							try {
								Class[] args = { Object.class };
								mSub = v1.getClass().getMethod("subsumption",
										args);
							} catch (NoSuchMethodException ex) {
								System.out.println("exception: "
										+ ex.getMessage());
								return false;
							}
							try {
								Object[] args = { v2 };
								b = ((Boolean) (m.invoke(v1, args)))
										.booleanValue();
							} catch (Exception ex) {
								System.out.println("exception: "
										+ ex.getMessage());
								return false;
							}
						}
						// It's enough to subsume one of the elements of the
						// possible Vector
						if (b)
							break;
					}
					if (!b) {
						return false;
					}
				}
			}
		} else {
			return false;
		}
		return true;
	}

	/** 
	 * Prints a simple string representation of a Feature Structure. 
	 */
	public void display() {
		String key;
		Enumeration e;
		e = this.getFields().keys(); // enumerates the labels of the feature
								// structure
		System.out.print("[ ");
		while (e.hasMoreElements()) {
			key = (String) (e.nextElement());
			Object v1 = get(key);
			System.out.print("\t"+key + " : ");
			if (v1 instanceof FeatureStructure) {
				((FeatureStructure) v1).display();
			} else if (v1 instanceof Variable) {
				((Variable) v1).display();
			} else if (v1 instanceof String) {
				System.out.println((String) v1);
			} else if (v1 != null) {
				v1.toString();
				System.out.println();
			}
		}
		System.out.print("] \n");
	}

	/**
	 * This method tries to instantiate the empty variables according to the
	 * information stored in a map representing the variables already found.
	 * @param variables
	 */
	public void fillEmptyVariables(Map variables) {
		String key;
		String name;
		Enumeration e;
		e = this.fields.keys(); // enumerates the labels of the feature structure
		while (e.hasMoreElements()) {
			key = (String) (e.nextElement());
			Object v1 = this.fields.get(key);
			if (v1 instanceof Variable) {
				if (((Variable) v1).isEmpty()) {
					name = ((Variable) v1).getName();
					if (variables.get(name) != null)
						((Variable) v1).setValue(variables.get(name));
				}
			} else if (v1 instanceof FeatureStructure) {
				((FeatureStructure) v1).fillEmptyVariables(variables);
			}
		}
	}


	/**
	 * This method stores the values of instantiated variables in a map
	 * representing the variables already found.
	 * @param variables
	 */
	public void saveVariables(Map variables) {
		String key;
		String name;
		Enumeration e;
		e = this.fields.keys(); // enumerates the labels of the feature structure
		while (e.hasMoreElements()) {
			key = (String) (e.nextElement());
			Object v1 = this.fields.get(key);
			if (v1 instanceof Variable) {
				if (!((Variable) v1).isEmpty()) {
					name = ((Variable) v1).getName();
					if (variables.get(name) == null)
						variables.put(name, ((Variable) v1).getValue());
				}
			} else if (v1 instanceof FeatureStructure) {
				((FeatureStructure) v1).saveVariables(variables);
			}
		}
	}

	/**
	 * This method makes a copy of the feature structure
	 */
	public Object clone() {
		Object newFS = new FeatureStructure();		
		String key;
		Enumeration e;
		e = this.getFields().keys();
		while (e.hasMoreElements()) {
			key = (String) (e.nextElement());
			Object v1 = this.get(key);
			// if the attrib value is a feature structure calls the procedure
			// recursively
			if (v1 instanceof FeatureStructure) {
				((FeatureStructure) newFS).put(key,
						((FeatureStructure) v1).clone());
			} else {
				// if the value of the attrib is a value
				if (v1 instanceof Variable) {
					// if the variable is empty instantiates a new empty value
					// with the same name in the new FS
					if (((Variable) v1).isEmpty()) {
						((FeatureStructure) newFS).put(key, new Variable(
								((Variable) v1).getName()));
					} else {
						Variable newVar = new Variable(
								((Variable) v1).getName());
						newVar.setValue(((Variable) v1).getValue());
						((FeatureStructure) newFS).put(key, newVar);
					}
				} else {
					((FeatureStructure) newFS).put(key, v1);
				}
			}
		}
		return newFS;
	}

	/**
	 * This method cleans the values of all the variables contained in a feature
	 * structure
	 */
	public void cleanVariables() {
		String key;
		Enumeration e;
		e = this.getFields().keys();
		while (e.hasMoreElements()) {
			key = (String) (e.nextElement());
			Object v1 = this.get(key);
			// if the attrib value is another feature structure calls the method
			// recursively
			if (v1 instanceof FeatureStructure)
				((FeatureStructure) v1).cleanVariables();
			// if the value of the attrib is a variable deletes its value
			if (v1 instanceof Variable)
				((Variable) v1).clean();
		}
	}

	/**
	 * 
	 * @param invokeOn
	 * @param methodName
	 * @param parameterClass
	 * @return
	 */
	private Method findSetMethodByHierarchy(Object invokeOn, String methodName,
			Class<?> parameterClass){
		try {
			if (invokeOn instanceof FSArray) {
				Class<?> toInvokeClass = invokeOn.getClass();
				Class<?>[] methodArgumentsClass = {int.class, org.apache.uima.cas.FeatureStructure.class};			
				Method m = (toInvokeClass).getMethod(methodName, methodArgumentsClass);				
				return m;				
			} else {
				Class<?> toInvokeClass = invokeOn.getClass();
				Class<?>[] methodArgumentsClass = {parameterClass};			
				Method m = (toInvokeClass).getMethod(methodName, methodArgumentsClass);
				return m;
			}			
		} catch (SecurityException e) {
			e.printStackTrace();
			return null;
		} catch (NoSuchMethodException e) {
			Class<?> methodArgumentsSuperClass = parameterClass.getSuperclass();				
			return findSetMethodByHierarchy(invokeOn, methodName, methodArgumentsSuperClass);
		}		
	}
	
	/**
	 * Create a new object described in the right-hand-side of the rule
	 * @param jcas
	 * @param matched
	 * @return created object
	 */
	public Object toObject(JCas jcas, 
			ArrayList<Annotation> matched) {
		try {
			Hashtable featuresHT = this.getFields();
			Object created = createEmptyObject(featuresHT, jcas);
			Enumeration featureNames = featuresHT.keys();
			while (featureNames.hasMoreElements()) {
				String feataureName = (String) (featureNames.nextElement());
				Object parameter = featuresHT.get(feataureName);
				String[] methodNameAndPosition = constructMethodName(feataureName);
				String methodName = methodNameAndPosition[0];
				Integer position = null;
				if (methodNameAndPosition[1] != null)
					position = Integer.parseInt(methodNameAndPosition[1]);			
				setField(created, methodName, parameter, position, jcas, 
						matched);
			}
			return created;
		} catch (SecurityException e) {
			e.printStackTrace();
			return null;
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
			return null;
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
			return null;
		} catch (NoSuchMethodException e) {
			e.printStackTrace();
			return null;
		} catch (InstantiationException e) {
			e.printStackTrace();
			return null;
		} catch (IllegalAccessException e) {
			e.printStackTrace();
			return null;
		} catch (InvocationTargetException e) {
			e.printStackTrace();
			return null;
		}
	}
	
	/**
	 * 
	 * @param invokeOn
	 * @param methodName
	 * @param parameter
	 * @param position
	 * @param jcas
	 * @param matched
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 * @throws InvocationTargetException
	 * @throws SecurityException
	 * @throws NoSuchMethodException
	 */
	private void setField(Object invokeOn, String methodName,
			Object parameter, Integer position,  JCas jcas, 
			ArrayList<Annotation> matched) 
			throws IllegalArgumentException, IllegalAccessException,
			InvocationTargetException, SecurityException, NoSuchMethodException{		
		if (parameter instanceof Integer) {
			setIntegerField(invokeOn, methodName, parameter);
		} else if (parameter instanceof Boolean) {
			setBooleanField(invokeOn, methodName, parameter);
		} else if (parameter instanceof FeatureStructure) {			
			parameter = ((FeatureStructure) parameter).toObject(jcas, matched);
			invokeMethod(invokeOn, methodName, parameter, position);			
		} else if ( parameter instanceof Variable ) { //caso in cui l' oggetto è una variabile
			String variableName = ((Variable) parameter).getName();
			if (variableName.equals("$0")) {
				setAllFeatureMatchedArray(invokeOn, methodName, jcas, matched);
			} else {
				Object variableValue = ((Variable) parameter).getValue();
				setField(invokeOn, methodName, variableValue, position, jcas, 
						matched);
			}
		} else if (parameter instanceof FSArray) {
			invokeMethod(invokeOn, methodName, parameter, position);
		} else { 
			invokeMethod(invokeOn, methodName, parameter, position);
		}
	}	
	
	/**
	 * 
	 * @param invokeOn
	 * @param methodName
	 * @param jcas
	 * @param matched
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 * @throws InvocationTargetException
	 */
	private void setAllFeatureMatchedArray(Object invokeOn, String methodName,
			JCas jcas, ArrayList<Annotation> matched) 
			throws IllegalArgumentException, IllegalAccessException, 
			InvocationTargetException {
		FSArray matchedArray = new FSArray(jcas, matched.size());
		for (int i=0; i<matched.size(); ++i)
			matchedArray.set(i, matched.get(i));
		invokeMethod(invokeOn, methodName, matchedArray, null);
	}

	/**
	 * 
	 * @param invokeOn
	 * @param methodName
	 * @param parameter
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 * @throws InvocationTargetException
	 * @throws SecurityException
	 * @throws NoSuchMethodException
	 */
	private void setIntegerField(Object invokeOn, String methodName,
			Object parameter) throws IllegalArgumentException,
			IllegalAccessException, InvocationTargetException,
			SecurityException, NoSuchMethodException {
		Integer intParameter = (Integer) parameter;
		Class<?> toInvokeClass = invokeOn.getClass();
		Class<?>[] methodArgumentsClass = { int.class };
		Method method = (toInvokeClass).getMethod(methodName,
				methodArgumentsClass);
		Object[] parametersObject = { intParameter.intValue() };
		method.invoke(invokeOn, parametersObject);
	}
	
	/**
	 * 
	 * @param invokeOn
	 * @param methodName
	 * @param parameter
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 * @throws InvocationTargetException
	 * @throws SecurityException
	 * @throws NoSuchMethodException
	 */
	private void setBooleanField(Object invokeOn, String methodName,
			Object parameter) throws IllegalArgumentException,
			IllegalAccessException, InvocationTargetException,
			SecurityException, NoSuchMethodException {

		Boolean boolParameter = (Boolean) parameter;
		Class<?> toInvokeClass = invokeOn.getClass();
		Class<?>[] methodArgumentsClass = { boolean.class };
		Method method = (toInvokeClass).getMethod(methodName,
				methodArgumentsClass);
		Object[] parametersObject = { boolParameter.booleanValue()};
		method.invoke(invokeOn, parametersObject);
	}
	
	/**
	 * 
	 * @param invokeOn
	 * @param methodName
	 * @param parameter
	 * @param position
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 * @throws InvocationTargetException
	 */
	private void invokeMethod(Object invokeOn, String methodName,
			Object parameter, Integer position) 
			throws IllegalArgumentException, IllegalAccessException, 
			InvocationTargetException {
		if (invokeOn instanceof FSArray) {
			Class<?> parameterClass = parameter.getClass();
			Method method = findSetMethodByHierarchy(invokeOn, methodName,
					parameterClass);
			FSArray array = (FSArray) invokeOn;			
			if (position != null) {
				Object[] parametersObject = { position.intValue(), parameter };
				method.invoke(invokeOn, parametersObject);				
			}			
		}
		else {
			Class<?> parameterClass = parameter.getClass();
			Method method = findSetMethodByHierarchy(invokeOn, methodName,
					parameterClass);
			Object[] parametersObject = { parameter };
			method.invoke(invokeOn, parametersObject);
		}		
	}	
	
	/**
	 * 
	 * @param featureName
	 * @return
	 */
	private String[] constructMethodName(String featureName) {
		String prefix;
		String realFeatureName;
		String[] methodNameAndPosition = new String[2];
		if (featureName.contains("#")) {
			methodNameAndPosition[0] = "set";
			methodNameAndPosition[1] = featureName.substring(
					featureName.length()-1,featureName.length());			
		} else {
			prefix = new String("set");
			realFeatureName = featureName;
			String firstLetter = realFeatureName.substring(0, 1).toUpperCase();
			realFeatureName = firstLetter
					+ realFeatureName.substring(1, realFeatureName.length());
			methodNameAndPosition[0] = prefix + realFeatureName;
			methodNameAndPosition[1] = null;
		}
		return methodNameAndPosition;
	}

	/**
	 * 
	 * create a new instance of class declared as Name of feature structure
	 * @param objectFieldsHT hash table that contains fields
	 * @param jcas JCas given by Analysis Engine 
	 * @return empty object of class specified in the feature name of FeatureStructure
	 * @throws ClassNotFoundException 
	 * @throws NoSuchMethodException 
	 * @throws SecurityException 
	 * @throws InvocationTargetException 
	 * @throws IllegalAccessException 
	 * @throws InstantiationException 
	 * @throws IllegalArgumentException 
	 * 
	 */
	private Object createEmptyObject(Hashtable objectFieldsHT, 
			JCas jcas) throws ClassNotFoundException, 
			SecurityException, NoSuchMethodException, 
			IllegalArgumentException, InstantiationException, 
			IllegalAccessException, InvocationTargetException {
		String className = (String) objectFieldsHT.get("__CLASS_NAME__");
		if (className.equals("org.apache.uima.jcas.cas.FSArray")) {
			int arraySize = objectFieldsHT.size()-1;
			Class classOfObject = Class.forName(className);
			Class[] constructorArgumentsClass = new Class[2];
			constructorArgumentsClass[0] = Class
					.forName("org.apache.uima.jcas.JCas");
			constructorArgumentsClass[1] = int.class;
			Constructor constructor = classOfObject
				.getConstructor(constructorArgumentsClass);
			Object[] constructorArgumentsObj = new Object[2];
			constructorArgumentsObj[0] = jcas;
			constructorArgumentsObj[1] = arraySize;
			Object createdObject = 
				constructor.newInstance(constructorArgumentsObj);			
			objectFieldsHT.remove("__CLASS_NAME__");
			return createdObject;			
		} else {
			Class classOfObject = Class.forName(className);
			Class[] constructorArgumentsClass = new Class[1];
			constructorArgumentsClass[0] = Class
					.forName("org.apache.uima.jcas.JCas");
			Constructor constructor = classOfObject
					.getConstructor(constructorArgumentsClass);
			Object[] constructorArgumentsObj = new Object[1];
			constructorArgumentsObj[0] = jcas;
			Object createdObject = constructor.newInstance(constructorArgumentsObj);
			objectFieldsHT.remove("__CLASS_NAME__");
			return createdObject;			
		}
		
	}
}
